package fserv.ni;

import fserv.entities.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Network Interface for the Chat System.
 * 
 * Opens a listening UDP socket in a thread, receives messages from remote peers
 * and forwards them to the controller.
 * Sends messages passed by the controller over the network.
 * 
 * @author Arthur Bit-Monnot <abit@etud.insa-toulouse.fr>
 * @author Julien Marchand <jmarchan@etud.insa-toulouse.fr>
 */
public class NetworkInterface implements CtrlToNI {

    /**
     * Port used by the server.
     */
    public static final int ServerPORT = 1711;
	/**
	 * Port used the application is listening on.
	 */
	private static int port = 0;
    /**
     * Size of the receive buffer.
     */
    private static final int BUFFER_MAX_LEN = 4096;
    /**
     * Unique instance of the network interface (Singleton Pattern).
     */
    private static NetworkInterface instance;
    /**
     * The broadcast address used to send messages.
     */
    private static InetAddress remoteAddr;

	/**
	 * @return the port
	 */
	public static int getPort() {
		return port;
	}

	/**
	 * @param aInPort the port to set
	 */
	public static void setPort(int aInPort) {
		port = aInPort;
	}
    /**
     * Reference to the chat controller.
     */
    private NIToCtrl ctrl;
    /**
     * UDP socket used to send messages to remote peers.
     */
    DatagramSocket socket;

    /**
     * Constructor for the network interface.
     * Determines the host address and the broadcast address, and opens
     * a sending UDP socket.
     */
    private NetworkInterface() {
		System.out.println("Remote addr " + NetworkInterface.remoteAddr);
    }

    /**
     * Configures a preferred broadcast address. Useful when the host is
     * connected to multiple networks.
     * 
     * @param remoteAddr the preferred broadcast address
     */
    public static void setRemoteAddr(InetAddress remoteAddr) {
        NetworkInterface.remoteAddr = remoteAddr;
    }

    /**
     * Returns the unique network interface instance (Singleton Pattern).
     * 
     * @return the network interface instance
     */
    public static NetworkInterface getInstance() {
        if (instance == null) {
            instance = new NetworkInterface();
        }
        return instance;
    }

    /**
     * Links the network interface to the controller.
     * 
     * @param ctrl the chat controller. Must implement the NIToCtrl interface.
     */
    public void setCtrl(NIToCtrl ctrl) {
        this.ctrl = ctrl;
    }

    /**
     * Broadcasts a message to all remote peers.
     * 
     * @param m the message to send
     */
    @Override
    public void send(Message m) {
        this.send(m, NetworkInterface.remoteAddr, ServerPORT);
    }

    /**
     * Sends a message to a specified remote peer.
     * 
     * @param m the message to send
     * @param addr the InetAddress of the recipient of the message
     */
    @Override
    public void send(Message m, InetAddress addr, int port) {
        try {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            m.writeToStream(baos);
            DatagramPacket packet = new DatagramPacket(baos.toByteArray(), baos.size(), addr, port);
            socket.send(packet);
        } catch (IOException ex) {
            Logger.getLogger(NetworkInterface.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    /**
     * Gracefully stops the network interface.
     * Closes the listening socket.
     */
    @Override
    public void stop(Thread t) {
        t.interrupt();
        socket.close();
        try {
            t.join();
        } catch (InterruptedException ex) {
        }
    }

    /**
     * Launches a thread, opens a listening UDP socket and forwards the received
     * messages to the controller.
     */
    @Override
    public void run() {
        Logger.getLogger(NetworkInterface.class.getName()).log(Level.INFO, "Starting.");

        try {
			if(port == 0) {
				this.socket = new DatagramSocket();
				NetworkInterface.port = this.socket.getPort();
			} else {
				this.socket = new DatagramSocket(NetworkInterface.port);
			}
        } catch (SocketException ex) {
            Logger.getLogger(NetworkInterface.class.getName()).log(Level.SEVERE, null, ex);
        }

        synchronized (this) {
            this.notify();
        }

        while (true) {
            try {
                byte[] recvBuffer = new byte[BUFFER_MAX_LEN];
                DatagramPacket packet = new DatagramPacket(recvBuffer, recvBuffer.length);
				socket.receive(packet);
				
				Message m;
				try {
					m = Message.fromStream(new ByteArrayInputStream(recvBuffer));
					m.setSourceAddr(packet.getAddress());
					m.setSourcePort(packet.getPort());
					
					if (m instanceof Reply) {
						ctrl.replyReceived((Reply) m);
					} else if (m instanceof Command) {
						ctrl.commandReceived((Command) m);
					} else if (m instanceof FileProposition) {
						ctrl.filePropositionReceived((FileProposition) m);
					}
				} catch (InvalidMsgIdException ex) {
					Logger.getLogger(NetworkInterface.class.getName()).log(Level.SEVERE, null, ex);
				}
			} catch (IOException ex) {
				if (Thread.currentThread().isInterrupted()) {
					Logger.getLogger(NetworkInterface.class.getName()).log(Level.INFO, "Graceful stop.");
					break;
				} else {
					Logger.getLogger(NetworkInterface.class.getName()).log(Level.SEVERE, null, ex);
				}
			}
		}
		
		Logger.getLogger(NetworkInterface.class.getName()).log(Level.INFO, "Exiting.");
    }
}